package org.xx.armory.spring5.security;

import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.spring5.mvc.BusinessLogicException;

import javax.servlet.http.HttpSession;
import java.util.Date;

import static org.springframework.security.web.WebAttributes.AUTHENTICATION_EXCEPTION;
import static org.xx.armory.commons.DebugUtils.dumpTraceBack;
import static org.xx.armory.spring5.mvc.ModelUtils.buildJsonView;

@Controller
public class AuthenticationController {
    /**
     * 在Session中记录连续认证失败次数的Key。
     */
    private static final String FAILED_COUNT_ATTRIBUTE = "sign-in-failed-count";
    /**
     * 在Session中记录最近一次认证失败的时间。
     */
    private static final String LAST_FAILED_TIME_ATTRIBUTE = "last-sign-in-failed-time";

    /**
     * 用于页面视图显示登录错误代码的Key。
     */
    private static final String ERROR_CODE_KEY = "errorCode";

    /**
     * 登录名和凭据不匹配。
     */
    private static final int EC_MISMATCH = 1;

    /**
     * 登录名和凭据多次不匹配。
     */
    private static final int EC_MISMATCH_FREQ = 2;

    /**
     * 其它原因导致认证失败。
     */
    private static final int EC_OTHER = 9;

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(AuthenticationController.class);

    @Value("${armory.security.jump-action-after-signed-out:home}")
    private JumpAction jumpActionAfterSignedOut;

    @GetMapping(value = "/sign-in",
                produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView setUpSignInHtml() {
        return new ModelAndView();
    }

    private int getErrorCode(
            WebRequest request,
            HttpSession session
    ) {
        final var ex_ = request.getAttribute(AUTHENTICATION_EXCEPTION, RequestAttributes.SCOPE_REQUEST);

        if (!(ex_ instanceof Throwable)) {
            logger.error("AUTHENTICATION_EXCEPTION expected a Throwable but {}??", ex_);
            return EC_OTHER;
        }

        final var ex = (Throwable) ex_;

        final var now = new Date();

        if (ex instanceof BadCredentialsException) {
            final var fCount_ = session.getAttribute(FAILED_COUNT_ATTRIBUTE);
            var fCount = fCount_ instanceof Number ? ((Number) fCount_).intValue() : 0;

            final var fTime_ = session.getAttribute(LAST_FAILED_TIME_ATTRIBUTE);
            var fTime = fTime_ instanceof Date ? (Date) fTime_ : null;
            if (fTime != null && fTime.after(DateUtils.addMinutes(now, -60))) {
                ++fCount;
            }

            if (fCount >= 2) {
                return EC_MISMATCH_FREQ;
            } else {
                session.setAttribute(FAILED_COUNT_ATTRIBUTE, fCount);
                return EC_MISMATCH;
            }
        } else {
            logger.warn("Cannot sign in\n{}", dumpTraceBack(ex));
            return EC_OTHER;
        }
    }

    private void clearErrorCode(
            HttpSession session
    ) {
        session.removeAttribute(FAILED_COUNT_ATTRIBUTE);
        session.removeAttribute(LAST_FAILED_TIME_ATTRIBUTE);
    }

    @PostMapping(value = "/signed-in",
                 produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView signedInHtml(
            WebRequest request,
            HttpSession session
    ) {
        // 认证成功，转向首页。
        this.logger.trace("Authentication succeeded");

        clearErrorCode(session);

        return new ModelAndView("redirect:/");
    }

    @PostMapping(value = "/signed-in",
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public ModelAndView signedIn(
            WebRequest request,
            HttpSession session
    ) {
        // 认证成功， 返回空View。
        this.logger.trace("Authentication succeeded");

        clearErrorCode(session);

        return buildJsonView(null);
    }

    @PostMapping(value = "/sign-in",
                 produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView signInHtml(
            WebRequest request,
            HttpSession session
    ) {
        // 认证失败，留在登录页，并记录具体的错误码。
        final var result = new ModelAndView();

        final var lastErrorCode = getErrorCode(request, session);

        result.addObject(ERROR_CODE_KEY, lastErrorCode);

        clearErrorCode(session);

        return result;
    }

    @PostMapping(value = "/sign-in",
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public ModelAndView signIn(
            WebRequest request,
            HttpSession session
    ) {
        // 认证失败，抛出带有具体错误码的异常。
        final var lastErrorCode = getErrorCode(request, session);

        clearErrorCode(session);

        throw new BusinessLogicException(lastErrorCode);
    }

    @RequestMapping(value = "/signed-out",
                    produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView signedOutHtml(
            HttpSession session
    ) {
        // 已撤销认证凭据。
        switch (this.jumpActionAfterSignedOut) {
            case Home:
                return new ModelAndView("redirect:/");
            case SignIn:
                return new ModelAndView("redirect:/sign-in");
            default:
                return new ModelAndView();
        }
    }

    @RequestMapping(value = "/signed-out",
                    produces = MediaType.APPLICATION_JSON_VALUE)
    public ModelAndView signedOut(
            HttpSession session
    ) {
        // 已撤销认证凭据，返回空View。
        return buildJsonView(null);
    }
}
